import sys
import wx
import os

from cStringIO import StringIO
from BitTornado.bencode import bencode, bdecode
from configobj import ConfigObj, ConfigObjError
from helpers import encodings

options = {}
options['create_empty'] = True
options['encoding'] = "utf-8"
options['default_encoding'] = "utf-8"

################################################################

class ConfigReader(object):
    """
    Read and write various types of config values.
    Values are converted to strings when writing
    and back into their respective types when reading.
    """
    def __init__(self, filename, section, defaults = None):
        if defaults is None:
            defaults = {}
      
        self.defaults = defaults

        self.defaultvalues = { "string"  : "",
                               "int"     : 0,
                               "float"   : 0.0,
                               "boolean" : False,
                               "color"   : wx.Colour(255, 255, 255),
                               "bencode-list" : [],
                               "bencode-dict" : {},
                               "bencode-string": "",
                               "bencode-fontinfo": {'name': None,
                                                    'size': None,
                                                    'style': None,
                                                    'weight': None }
                              }

        self.filename = filename
        self.section = section
               
        # If the directory for this file doesn't exist,
        # try creating it now
        dirname = os.path.dirname(self.filename)
        if not os.access(dirname, os.F_OK):
            os.makedirs(dirname)

        self.configobj = None
        
        self.getConfigObj()
        
    def getConfigObj(self, encoding = None, firsttry = True):
        newoptions = options.copy()
        if encoding is not None:
            newoptions['encoding'] = encoding

        try:
            self.configobj = ConfigObj(self.filename, options = newoptions)
            
            # If we had to fall back on another encoding to read,
            # switch back to UTF-8 for writing
            if encoding is not None:
                self.configobj.encoding = "utf-8"
                
            return
        except UnicodeDecodeError:
            # Second try -- shouldn't be getting a Unicode error now
            if encoding is not None:
                sys.stderr.write("Error converting: " + self.filename.encode(sys.getfilesystemencoding()) + " to unicode!\n")
                sys.stderr.write("(Try converting to UTF-8 and try again.\n")
                return
                
            # Try other encodings
            encoding = self.testEncodings()
            if encoding is not None:
                # Got an encoding that seems to work, give it a try
                self.getConfigObj(encoding)
                return
            else:
                sys.stderr.write("Unable to convert file: " + self.filename.encode(sys.getfilesystemencoding()) + " to unicode!\n")
                sys.stderr.write("(Try converting to UTF-8 and try again.)\n")
        except ConfigObjError, configerr:
            if not firsttry:
                sys.stderr.write("\n")
                sys.stderr.write("Error reading file: " + self.filename.encode(sys.getfilesystemencoding()) + "\n")
                sys.stderr.write("Message: " + configerr.message + "\n")
                sys.stderr.write("\n")
        except:
            if not firsttry:
                sys.stderr.write("\n")
                sys.stderr.write("Error reading file: " + self.filename.encode(sys.getfilesystemencoding()) + "\n")
                sys.stderr.write("\n")
            
        if self.configobj is None:
            if firsttry:
                # First attempt failed, try to see if we can repair the file
                self.tryRepair()
                self.getConfigObj(encoding = encoding, firsttry = False)
            else:
                # Should never get here... (hopefully)
            
                # Just in case, make an empty ConfigObj object
                self.configobj = ConfigObj(options = newoptions)
                
                # If we had to fall back on another encoding to read,
                # switch back to UTF-8 for writing
                if encoding is not None:
                    self.configobj.encoding = "utf-8"
                self.configobj.filename = self.filename
                
    def testEncodings(self):               
        """
        If UTF-8 doesn't work, try some other common encodings to see if one of them will work
        """
        for encoding in encodings:
            newoptions = options.copy()
            newoptions['encoding'] = encoding
            try:
                testconfig = ConfigObj(self.filename, options = newoptions)                            
                return encoding
            except UnicodeDecodeError:
                # Encoding didn't work
                pass
            except:
                # Encoding seems to have worked, but some other problem was present
                return encoding
        
        return None
        
    def testConfig(self, goodconfig, newline, passes = 0, encoding = None):
        """
        Slow but sure analysis of a config file
        (Try adding in each line one at a time to see if they cause problems)
        """
        if not newline:
            return goodconfig
        
        newoptions = options.copy()
        if encoding is not None:
            newoptions['encoding'] = encoding
               
        testconfig = goodconfig + newline + "\n"
        
        # Write out to a StringIO object
        newfile = StringIO(testconfig)
        try:
            # See if it works as a ConfigObj
            testparser = ConfigObj(newfile, options = newoptions)               
            
            # Line looks ok, add it to the config file
            return testconfig
        except:
            # Ignore the line, don't add it to the config file
            pass
            
        return goodconfig
    
    def tryRepair(self, encoding = None):
        """
        Try to repair a damaged config file
        (i.e.: one w/ parsing errors, etc.)
        """
        if self.filename[-5:] == ".lang":
            # (Don't try to fix language files)
            return
        
        sys.stderr.write("Attempting to fix (some lines may be lost)\n")
        sys.stderr.write("(original file saved as " + self.filename.encode(sys.getfilesystemencoding()) + ".backup" + "\n")        

        oldconfig = ""
        
        try:
            oldfile = open(self.filename, "r")
            oldconfig = oldfile.readlines()
            oldfile.close()
        except:
            # Can't read the original file at all
            #
            # try to write a blank file with just the section header
            newfile = open(self.filename, "w")
            newfile.write("[" + self.section + "]\n")
            newfile.close()
            return
            
        goodconfig = ""
        
        for line in oldconfig:
            # Strip off any leading or trailing spaces
            newline = line.strip()
            
            # If the line looks ok, try writing it
            goodconfig = self.testConfig(goodconfig, newline, encoding = encoding)

        # Make a backup of the original file
        newfile = open(self.filename + ".backup", "w")
        newfile.writelines(oldconfig)
        newfile.close()

        newfile = open(self.filename, "w")
        newfile.writelines(goodconfig)
        newfile.close()
            
    def setSection(self, section):
        """
        Set the default section
        """
        self.section = section

    def ValueToString(self, value, type):
        """
        Convert a value to a string
        """
        # If it's a string, don't worry about converting to a string
        if isinstance(value, basestring):
            return value
        
        if type == "boolean":
            if value:
                text = "1"
            else:
                text = "0"
        elif type == "color":
            red = str(value.Red())
            while len(red) < 3:
                red = "0" + red

            green = str(value.Green())            
            while len(green) < 3:
                green = "0" + green
                
            blue = str(value.Blue())            
            while len(blue) < 3:
                blue = "0" + blue

            text = str(red) + str(green) + str(blue)
        elif type.startswith("bencode"):
            text = bencode(value)
        else:
            try:
                text = str(value)
            except:
                # Something went wrong with conversion
                text = ""
        
        return text

    def StringToValue(self, value, type):
        """
        Convert a string into the specified type
        """
        # Assume that the value is already in the proper form
        # if it's not a string
        # (the case for some defaults)
        #
        # using isinstance(value, basestring) should work for both
        # string and unicode types
        if value is not None and not isinstance(value, basestring):
            return value

        try:
            if type == "boolean":
                if value == "1":
                    value = True
                else:
                    value = False
            elif type == "int":
                value = int(value)
            elif type == "float":
                value = float(value)
            elif type == "color":
                red = int(value[0:3])
                green = int(value[3:6])
                blue = int(value[6:9])
                value = wx.Colour(red, green, blue)
            elif type.startswith("bencode"):
                value = bdecode(value)
        except:           
            value = None
            
        if value is None:
            value = self.defaultvalues[type]
        
        return value

    def ReadDefault(self, param, type = "string", section = None):
        if section is None:
            section = self.section

        if param is None or param == "":
            return ""

        param = param.lower()
        value = self.defaults.get(param, None)
            
        value = self.StringToValue(value, type)
            
        return value
        
    def Read(self, param, type = "string", section = None):
        if section is None:
            section = self.section
            
        if param is None or param == "":
            return ""
        
        try:
            value = self.configobj[section][param]
        except:
            if not self.Exists(param, section):
                value = self.ReadDefault(param, section = section)
            else:
                print
                print "!!!!!!!!!!"
                print "label: " + param
                print
            
                from traceback import format_exc

                sys.stderr.write(format_exc())
                value = ""

        value = self.StringToValue(value, type)
           
        return value
        
    def Exists(self, param, section = None):
        """
        See if a parameter exists in a section
        """
        param = param.lower()
        
        if section is None:
            section = self.section
        
        # If the section isn't present,
        # then the paramter won't be either
        if not section in self.configobj:
            return False
        
        return param in self.configobj[section]
    
    def GetOptions(self, section = None):
        if section is None:
            section = self.section
        try:
            options = self.configobj[section].keys()
        except:
            options = []
            
        return options
        
    def Items(self, section = None):
        """"
        Get the list of items in a section
        """
        
        if section is None:
            section = self.section
            
        items = []
        
        try:
            for key in self.configobj[section]:
                value = self.Read(key, section = section)
                items.append((key, value))
            return items
        except:
            self.configobj[section] = {}

        return []
        
    def Write(self, param, value, type = "string", section = None):
        """
        Write out a new value for a parameter
        """
        if section is None:
            section = self.section
            
        if param is None or param == "":     
            return False
        
        param = param.lower()
        
        if not section in self.configobj:
            self.configobj[section] = {}
               
        text = self.ValueToString(value, type)
        
        text_old = self.Read(param, section = section)
        text_default = self.ReadDefault(param, section = section)
        
        if text == text_default:
            # Don't store default values
            return self.DeleteEntry(param, section)
        elif text != text_old:
            # Value needs to be changed
            self.configobj[section][param] = text
            return True
        
        return False
    
    def DeleteEntry(self, param, section = None):
        """
        Delete an entry from a section
        """
        if section is None:
            section = self.section
               
        try:
            del self.configobj[section][param]
            return True
        except:
            return False
        
    def DeleteGroup(self, section = None):
        """
        Delete an entire section
        """
        if section is None:
            section = self.section

        try:
            del self.configobj[section]
            return True
        except:
            return False
        
    def Flush(self):
        """
        Write out config values to disk
        """
        self.configobj.write()
